home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 10868 / 10868.xpi / modules / engines / bookmarks.js next >
Text File  |  2010-02-02  |  38KB  |  1,160 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Bookmarks Sync.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Dan Mills <thunder@mozilla.com>
  22.  *  Jono DiCarlo <jdicarlo@mozilla.org>
  23.  *  Anant Narayanan <anant@kix.in>
  24.  *
  25.  * Alternatively, the contents of this file may be used under the terms of
  26.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  27.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  28.  * in which case the provisions of the GPL or the LGPL are applicable instead
  29.  * of those above. If you wish to allow use of your version of this file only
  30.  * under the terms of either the GPL or the LGPL, and not to allow others to
  31.  * use your version of this file under the terms of the MPL, indicate your
  32.  * decision by deleting the provisions above and replace them with the notice
  33.  * and other provisions required by the GPL or the LGPL. If you do not delete
  34.  * the provisions above, a recipient may use your version of this file under
  35.  * the terms of any one of the MPL, the GPL or the LGPL.
  36.  *
  37.  * ***** END LICENSE BLOCK ***** */
  38.  
  39. const EXPORTED_SYMBOLS = ['BookmarksEngine', 'BookmarksSharingManager'];
  40.  
  41. const Cc = Components.classes;
  42. const Ci = Components.interfaces;
  43. const Cu = Components.utils;
  44.  
  45. const PARENT_ANNO = "weave/parent";
  46. const PREDECESSOR_ANNO = "weave/predecessor";
  47. const SERVICE_NOT_SUPPORTED = "Service not supported on this platform";
  48.  
  49. Cu.import("resource://gre/modules/utils.js");
  50. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  51. Cu.import("resource://weave/ext/Observers.js");
  52. Cu.import("resource://weave/util.js");
  53. Cu.import("resource://weave/engines.js");
  54. Cu.import("resource://weave/stores.js");
  55. Cu.import("resource://weave/trackers.js");
  56. Cu.import("resource://weave/type_records/bookmark.js");
  57.  
  58. // Lazily initialize the special top level folders
  59. let kSpecialIds = {};
  60. [["menu", "bookmarksMenuFolder"],
  61.  ["places", "placesRoot"],
  62.  ["tags", "tagsFolder"],
  63.  ["toolbar", "toolbarFolder"],
  64.  ["unfiled", "unfiledBookmarksFolder"],
  65. ].forEach(function([guid, placeName]) {
  66.   Utils.lazy2(kSpecialIds, guid, function() Svc.Bookmark[placeName]);
  67. });
  68. Utils.lazy2(kSpecialIds, "mobile", function() {
  69.   // Use the (one) mobile root if it already exists
  70.   let anno = "mobile/bookmarksRoot";
  71.   let root = Svc.Annos.getItemsWithAnnotation(anno, {});
  72.   if (root.length != 0)
  73.     return root[0];
  74.  
  75.   // Create the special mobile folder to store mobile bookmarks
  76.   let mobile = Svc.Bookmark.createFolder(Svc.Bookmark.placesRoot, "mobile", -1);
  77.   Utils.anno(mobile, anno, 1);
  78.   return mobile;
  79. });
  80.  
  81. // Create some helper functions to convert GUID/ids
  82. function idForGUID(guid) {
  83.   if (guid in kSpecialIds)
  84.     return kSpecialIds[guid];
  85.   return Svc.Bookmark.getItemIdForGUID(guid);
  86. }
  87. function GUIDForId(placeId) {
  88.   for (let [guid, id] in Iterator(kSpecialIds))
  89.     if (placeId == id)
  90.       return guid;
  91.   return Svc.Bookmark.getItemGUID(placeId);
  92. }
  93.  
  94. function BookmarksEngine() {
  95.   this._init();
  96. }
  97. BookmarksEngine.prototype = {
  98.   __proto__: SyncEngine.prototype,
  99.   name: "bookmarks",
  100.   _displayName: "Bookmarks",
  101.   description: "Keep your favorite links always at hand",
  102.   logName: "Bookmarks",
  103.   _recordObj: PlacesItem,
  104.   _storeObj: BookmarksStore,
  105.   _trackerObj: BookmarksTracker,
  106.  
  107.   _init: function _init() {
  108.     SyncEngine.prototype._init.call(this);
  109.     this._handleImport();
  110.   },
  111.  
  112.   _handleImport: function _handleImport() {
  113.     Observers.add("bookmarks-restore-begin", function() {
  114.       this._log.debug("Ignoring changes from importing bookmarks");
  115.       this._tracker.ignoreAll = true;
  116.     }, this);
  117.  
  118.     Observers.add("bookmarks-restore-success", function() {
  119.       this._log.debug("Triggering fresh start on successful import");
  120.       this.resetLastSync();
  121.       this._tracker.ignoreAll = false;
  122.     }, this);
  123.  
  124.     Observers.add("bookmarks-restore-failed", function() {
  125.       this._tracker.ignoreAll = false;
  126.     }, this);
  127.   },
  128.  
  129.   _sync: Utils.batchSync("Bookmark", SyncEngine),
  130.  
  131.   _syncStartup: function _syncStart() {
  132.     SyncEngine.prototype._syncStartup.call(this);
  133.  
  134.     // Lazily create a mapping of folder titles and separator positions to GUID
  135.     this.__defineGetter__("_lazyMap", function() {
  136.       delete this._lazyMap;
  137.  
  138.       let lazyMap = {};
  139.       for (let guid in this._store.getAllIDs()) {
  140.         // Figure out what key to store the mapping
  141.         let key;
  142.         let id = idForGUID(guid);
  143.         switch (Svc.Bookmark.getItemType(id)) {
  144.           case Svc.Bookmark.TYPE_BOOKMARK:
  145.             key = "b" + Svc.Bookmark.getBookmarkURI(id).spec + ":" +
  146.               Svc.Bookmark.getItemTitle(id);
  147.             break;
  148.           case Svc.Bookmark.TYPE_FOLDER:
  149.             key = "f" + Svc.Bookmark.getItemTitle(id);
  150.             break;
  151.           case Svc.Bookmark.TYPE_SEPARATOR:
  152.             key = "s" + Svc.Bookmark.getItemIndex(id);
  153.             break;
  154.           default:
  155.             continue;
  156.         }
  157.  
  158.         // The mapping is on a per parent-folder-name basis
  159.         let parent = Svc.Bookmark.getFolderIdForItem(id);
  160.         let parentName = Svc.Bookmark.getItemTitle(parent);
  161.         if (lazyMap[parentName] == null)
  162.           lazyMap[parentName] = {};
  163.  
  164.         // Remember this item's guid for its parent-name/key pair
  165.         lazyMap[parentName][key] = guid;
  166.       }
  167.  
  168.       // Expose a helper function to get a dupe guid for an item
  169.       return this._lazyMap = function(item) {
  170.         // Figure out if we have something to key with
  171.         let key;
  172.         switch (item.type) {
  173.           case "bookmark":
  174.           case "query":
  175.           case "microsummary":
  176.             key = "b" + item.bmkUri + ":" + item.title;
  177.             break;
  178.           case "folder":
  179.           case "livemark":
  180.             key = "f" + item.title;
  181.             break;
  182.           case "separator":
  183.             key = "s" + item.pos;
  184.             break;
  185.           default:
  186.             return;
  187.         }
  188.  
  189.         // Give the guid if we have the matching pair
  190.         this._log.trace("Finding mapping: " + item.parentName + ", " + key);
  191.         let parent = lazyMap[item.parentName];
  192.         let dupe = parent && parent[key];
  193.         this._log.trace("Mapped dupe: " + dupe);
  194.         return dupe;
  195.       };
  196.     });
  197.   },
  198.  
  199.   _syncFinish: function _syncFinish() {
  200.     SyncEngine.prototype._syncFinish.call(this);
  201.     delete this._lazyMap;
  202.     this._tracker._ensureMobileQuery();
  203.   },
  204.  
  205.   _findDupe: function _findDupe(item) {
  206.     return this._lazyMap(item);
  207.   },
  208.  
  209.   _handleDupe: function _handleDupe(item, dupeId) {
  210.     // The local dupe has the lower id, so make it the winning id
  211.     if (dupeId < item.id)
  212.       [item.id, dupeId] = [dupeId, item.id];
  213.  
  214.     // Trigger id change from dupe to winning and update the server
  215.     this._store.changeItemID(dupeId, item.id);
  216.     this._deleteId(dupeId);
  217.     this._tracker.changedIDs[item.id] = true;
  218.   }
  219. };
  220.  
  221. function BookmarksStore() {
  222.   this._init();
  223. }
  224. BookmarksStore.prototype = {
  225.   __proto__: Store.prototype,
  226.   name: "bookmarks",
  227.   _logName: "BStore",
  228.  
  229.   __bms: null,
  230.   get _bms() {
  231.     if (!this.__bms)
  232.       this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  233.                    getService(Ci.nsINavBookmarksService);
  234.     return this.__bms;
  235.   },
  236.  
  237.   __hsvc: null,
  238.   get _hsvc() {
  239.     if (!this.__hsvc)
  240.       this.__hsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
  241.                     getService(Ci.nsINavHistoryService);
  242.     return this.__hsvc;
  243.   },
  244.  
  245.   __ls: null,
  246.   get _ls() {
  247.     if (!this.__ls)
  248.       this.__ls = Cc["@mozilla.org/browser/livemark-service;2"].
  249.         getService(Ci.nsILivemarkService);
  250.     return this.__ls;
  251.   },
  252.  
  253.   get _ms() {
  254.     let ms;
  255.     try {
  256.       ms = Cc["@mozilla.org/microsummary/service;1"].
  257.         getService(Ci.nsIMicrosummaryService);
  258.     } catch (e) {
  259.       ms = null;
  260.       this._log.warn("Could not load microsummary service");
  261.       this._log.debug(e);
  262.     }
  263.     this.__defineGetter__("_ms", function() ms);
  264.     return ms;
  265.   },
  266.  
  267.   __ts: null,
  268.   get _ts() {
  269.     if (!this.__ts)
  270.       this.__ts = Cc["@mozilla.org/browser/tagging-service;1"].
  271.                   getService(Ci.nsITaggingService);
  272.     return this.__ts;
  273.   },
  274.  
  275.  
  276.   itemExists: function BStore_itemExists(id) {
  277.     return idForGUID(id) > 0;
  278.   },
  279.  
  280.   // Hash of old GUIDs to the new renamed GUIDs
  281.   aliases: {},
  282.  
  283.   applyIncoming: function BStore_applyIncoming(record) {
  284.     // Ignore (accidental?) root changes
  285.     if (record.id in kSpecialIds) {
  286.       this._log.debug("Skipping change to root node: " + record.id);
  287.       return;
  288.     }
  289.  
  290.     // Convert GUID fields to the aliased GUID if necessary
  291.     ["id", "parentid", "predecessorid"].forEach(function(field) {
  292.       let alias = this.aliases[record[field]];
  293.       if (alias != null)
  294.         record[field] = alias;
  295.     }, this);
  296.  
  297.     // Preprocess the record before doing the normal apply
  298.     switch (record.type) {
  299.       case "query": {
  300.         // Convert the query uri if necessary
  301.         if (record.bmkUri == null || record.folderName == null)
  302.           break;
  303.  
  304.         // Tag something so that the tag exists
  305.         let tag = record.folderName;
  306.         let dummyURI = Utils.makeURI("about:weave#BStore_preprocess");
  307.         this._ts.tagURI(dummyURI, [tag]);
  308.  
  309.         // Look for the id of the tag (that might have just been added)
  310.         let tags = this._getNode(this._bms.tagsFolder);
  311.         if (!(tags instanceof Ci.nsINavHistoryQueryResultNode))
  312.           break;
  313.  
  314.         tags.containerOpen = true;
  315.         for (let i = 0; i < tags.childCount; i++) {
  316.           let child = tags.getChild(i);
  317.           // Found the tag, so fix up the query to use the right id
  318.           if (child.title == tag) {
  319.             this._log.debug("query folder: " + tag + " = " + child.itemId);
  320.             record.bmkUri = record.bmkUri.replace(/([:&]folder=)\d+/, "$1" +
  321.               child.itemId);
  322.             break;
  323.           }
  324.         }
  325.         break;
  326.       }
  327.     }
  328.  
  329.     // Figure out the local id of the parent GUID if available
  330.     let parentGUID = record.parentid;
  331.     record._orphan = false;
  332.     if (parentGUID != null) {
  333.       let parentId = idForGUID(parentGUID);
  334.  
  335.       // Default to unfiled if we don't have the parent yet
  336.       if (parentId <= 0) {
  337.         this._log.trace("Reparenting to unfiled until parent is synced");
  338.         record._orphan = true;
  339.         parentId = kSpecialIds.unfiled;
  340.       }
  341.  
  342.       // Save the parent id for modifying the bookmark later
  343.       record._parent = parentId;
  344.     }
  345.  
  346.     // Default to append unless we're not an orphan with the predecessor
  347.     let predGUID = record.predecessorid;
  348.     record._insertPos = Svc.Bookmark.DEFAULT_INDEX;
  349.     if (!record._orphan) {
  350.       // No predecessor means it's the first item
  351.       if (predGUID == null)
  352.         record._insertPos = 0;
  353.       else {
  354.         // The insert position is one after the predecessor of the same parent
  355.         let predId = idForGUID(predGUID);
  356.         if (predId != -1 && this._getParentGUIDForId(predId) == parentGUID) {
  357.           record._insertPos = Svc.Bookmark.getItemIndex(predId) + 1;
  358.           record._predId = predId;
  359.         }
  360.         else
  361.           this._log.trace("Appending to end until predecessor is synced");
  362.       }
  363.     }
  364.  
  365.     // Do the normal processing of incoming records
  366.     Store.prototype.applyIncoming.apply(this, arguments);
  367.  
  368.     // Do some post-processing if we have an item
  369.     let itemId = idForGUID(record.id);
  370.     if (itemId > 0) {
  371.       // Move any children that are looking for this folder as a parent
  372.       if (record.type == "folder")
  373.         this._reparentOrphans(itemId);
  374.  
  375.       // Create an annotation to remember that it needs a parent
  376.       // XXX Work around Bug 510628 by prepending parenT
  377.       if (record._orphan)
  378.         Utils.anno(itemId, PARENT_ANNO, "T" + parentGUID);
  379.       // It's now in the right folder, so move annotated items behind this
  380.       else
  381.         this._attachFollowers(itemId);
  382.  
  383.       // Create an annotation if we have a predecessor but no position
  384.       // XXX Work around Bug 510628 by prepending predecessoR
  385.       if (predGUID != null && record._insertPos == Svc.Bookmark.DEFAULT_INDEX)
  386.         Utils.anno(itemId, PREDECESSOR_ANNO, "R" + predGUID);
  387.     }
  388.   },
  389.  
  390.   /**
  391.    * Find all ids of items that have a given value for an annotation
  392.    */
  393.   _findAnnoItems: function BStore__findAnnoItems(anno, val) {
  394.     // XXX Work around Bug 510628 by prepending parenT
  395.     if (anno == PARENT_ANNO)
  396.       val = "T" + val;
  397.     // XXX Work around Bug 510628 by prepending predecessoR
  398.     else if (anno == PREDECESSOR_ANNO)
  399.       val = "R" + val;
  400.  
  401.     return Svc.Annos.getItemsWithAnnotation(anno, {}).filter(function(id)
  402.       Utils.anno(id, anno) == val);
  403.   },
  404.  
  405.   /**
  406.    * For the provided parent item, attach its children to it
  407.    */
  408.   _reparentOrphans: function _reparentOrphans(parentId) {
  409.     // Find orphans and reunite with this folder parent
  410.     let parentGUID = GUIDForId(parentId);
  411.     let orphans = this._findAnnoItems(PARENT_ANNO, parentGUID);
  412.  
  413.     this._log.debug("Reparenting orphans " + orphans + " to " + parentId);
  414.     orphans.forEach(function(orphan) {
  415.       // Append the orphan under the parent unless it's supposed to be first
  416.       let insertPos = Svc.Bookmark.DEFAULT_INDEX;
  417.       if (!Svc.Annos.itemHasAnnotation(orphan, PREDECESSOR_ANNO))
  418.         insertPos = 0;
  419.  
  420.       // Move the orphan to the parent and drop the missing parent annotation
  421.       Svc.Bookmark.moveItem(orphan, parentId, insertPos);
  422.       Svc.Annos.removeItemAnnotation(orphan, PARENT_ANNO);
  423.     });
  424.  
  425.     // Fix up the ordering of the now-parented items
  426.     orphans.forEach(this._attachFollowers, this);
  427.   },
  428.  
  429.   /**
  430.    * Move an item and all of its followers to a new position until reaching an
  431.    * item that shouldn't be moved
  432.    */
  433.   _moveItemChain: function BStore__moveItemChain(itemId, insertPos, stopId) {
  434.     let parentId = Svc.Bookmark.getFolderIdForItem(itemId);
  435.  
  436.     // Keep processing the item chain until it loops to the stop item
  437.     do {
  438.       // Figure out what's next in the chain
  439.       let itemPos = Svc.Bookmark.getItemIndex(itemId);
  440.       let nextId = Svc.Bookmark.getIdForItemAt(parentId, itemPos + 1);
  441.  
  442.       Svc.Bookmark.moveItem(itemId, parentId, insertPos);
  443.       this._log.trace("Moved " + itemId + " to " + insertPos);
  444.  
  445.       // Prepare for the next item in the chain
  446.       insertPos = Svc.Bookmark.getItemIndex(itemId) + 1;
  447.       itemId = nextId;
  448.  
  449.       // Stop if we ran off the end or the item is looking for its pred.
  450.       if (itemId == -1 || Svc.Annos.itemHasAnnotation(itemId, PREDECESSOR_ANNO))
  451.         break;
  452.     } while (itemId != stopId);
  453.   },
  454.  
  455.   /**
  456.    * For the provided predecessor item, attach its followers to it
  457.    */
  458.   _attachFollowers: function BStore__attachFollowers(predId) {
  459.     let predGUID = GUIDForId(predId);
  460.     let followers = this._findAnnoItems(PREDECESSOR_ANNO, predGUID);
  461.     if (followers.length > 1)
  462.       this._log.warn(predId + " has more than one followers: " + followers);
  463.  
  464.     // Start at the first follower and move the chain of followers
  465.     let parent = Svc.Bookmark.getFolderIdForItem(predId);
  466.     followers.forEach(function(follow) {
  467.       this._log.trace("Repositioning " + follow + " behind " + predId);
  468.       if (Svc.Bookmark.getFolderIdForItem(follow) != parent) {
  469.         this._log.warn("Follower doesn't have the same parent: " + parent);
  470.         return;
  471.       }
  472.  
  473.       // Move the chain of followers to after the predecessor
  474.       let insertPos = Svc.Bookmark.getItemIndex(predId) + 1;
  475.       this._moveItemChain(follow, insertPos, predId);
  476.  
  477.       // Remove the annotation now that we're putting it in the right spot
  478.       Svc.Annos.removeItemAnnotation(follow, PREDECESSOR_ANNO);
  479.     }, this);
  480.   },
  481.  
  482.   create: function BStore_create(record) {
  483.     let newId;
  484.     switch (record.type) {
  485.     case "bookmark":
  486.     case "query":
  487.     case "microsummary": {
  488.       let uri = Utils.makeURI(record.bmkUri);
  489.       newId = this._bms.insertBookmark(record._parent, uri, record._insertPos,
  490.         record.title);
  491.       this._log.debug(["created bookmark", newId, "under", record._parent, "at",
  492.         record._insertPos, "as", record.title, record.bmkUri].join(" "));
  493.  
  494.       this._tagURI(uri, record.tags);
  495.       this._bms.setKeywordForBookmark(newId, record.keyword);
  496.       if (record.description)
  497.         Utils.anno(newId, "bookmarkProperties/description", record.description);
  498.  
  499.       if (record.loadInSidebar)
  500.         Utils.anno(newId, "bookmarkProperties/loadInSidebar", true);
  501.  
  502.       if (record.type == "microsummary") {
  503.         this._log.debug("   \-> is a microsummary");
  504.         Utils.anno(newId, "bookmarks/staticTitle", record.staticTitle || "");
  505.         let genURI = Utils.makeURI(record.generatorUri);
  506.     if (this._ms) {
  507.           try {
  508.             let micsum = this._ms.createMicrosummary(uri, genURI);
  509.             this._ms.setMicrosummary(newId, micsum);
  510.           }
  511.           catch(ex) { /* ignore "missing local generator" exceptions */ }
  512.     } else {
  513.       this._log.warn("Can't create microsummary -- not supported.");
  514.     }
  515.       }
  516.     } break;
  517.     case "folder":
  518.       newId = this._bms.createFolder(record._parent, record.title,
  519.         record._insertPos);
  520.       this._log.debug(["created folder", newId, "under", record._parent, "at",
  521.         record._insertPos, "as", record.title].join(" "));
  522.       break;
  523.     case "livemark":
  524.       let siteURI = null;
  525.       if (record.siteUri != null)
  526.         siteURI = Utils.makeURI(record.siteUri);
  527.  
  528.       newId = this._ls.createLivemark(record._parent, record.title, siteURI,
  529.         Utils.makeURI(record.feedUri), record._insertPos);
  530.       this._log.debug(["created livemark", newId, "under", record._parent, "at",
  531.         record._insertPos, "as", record.title, record.siteUri, record.feedUri].
  532.         join(" "));
  533.       break;
  534.     case "separator":
  535.       newId = this._bms.insertSeparator(record._parent, record._insertPos);
  536.       this._log.debug(["created separator", newId, "under", record._parent,
  537.         "at", record._insertPos].join(" "));
  538.       break;
  539.     case "item":
  540.       this._log.debug(" -> got a generic places item.. do nothing?");
  541.       return;
  542.     default:
  543.       this._log.error("_create: Unknown item type: " + record.type);
  544.       return;
  545.     }
  546.  
  547.     this._log.trace("Setting GUID of new item " + newId + " to " + record.id);
  548.     this._setGUID(newId, record.id);
  549.   },
  550.  
  551.   remove: function BStore_remove(record) {
  552.     let itemId = idForGUID(record.id);
  553.     if (itemId <= 0) {
  554.       this._log.debug("Item " + record.id + " already removed");
  555.       return;
  556.     }
  557.     var type = this._bms.getItemType(itemId);
  558.  
  559.     switch (type) {
  560.     case this._bms.TYPE_BOOKMARK:
  561.       this._log.debug("  -> removing bookmark " + record.id);
  562.       this._ts.untagURI(this._bms.getBookmarkURI(itemId), null);
  563.       this._bms.removeItem(itemId);
  564.       break;
  565.     case this._bms.TYPE_FOLDER:
  566.       this._log.debug("  -> removing folder " + record.id);
  567.       Svc.Bookmark.removeItem(itemId);
  568.       break;
  569.     case this._bms.TYPE_SEPARATOR:
  570.       this._log.debug("  -> removing separator " + record.id);
  571.       this._bms.removeItem(itemId);
  572.       break;
  573.     default:
  574.       this._log.error("remove: Unknown item type: " + type);
  575.       break;
  576.     }
  577.   },
  578.  
  579.   update: function BStore_update(record) {
  580.     let itemId = idForGUID(record.id);
  581.  
  582.     if (itemId <= 0) {
  583.       this._log.debug("Skipping update for unknown item: " + record.id);
  584.       return;
  585.     }
  586.  
  587.     this._log.trace("Updating " + record.id + " (" + itemId + ")");
  588.  
  589.     // Move the bookmark to a new parent if necessary
  590.     if (Svc.Bookmark.getFolderIdForItem(itemId) != record._parent) {
  591.       this._log.trace("Moving item to a new parent");
  592.       Svc.Bookmark.moveItem(itemId, record._parent, record._insertPos);
  593.     }
  594.     // Move the chain of bookmarks to a new position
  595.     else if (Svc.Bookmark.getItemIndex(itemId) != record._insertPos &&
  596.              !record._orphan) {
  597.       this._log.trace("Moving item and followers to a new position");
  598.  
  599.       // Stop moving at the predecessor unless we don't have one
  600.       this._moveItemChain(itemId, record._insertPos, record._predId || itemId);
  601.     }
  602.  
  603.     for (let [key, val] in Iterator(record.cleartext)) {
  604.       switch (key) {
  605.       case "title":
  606.         this._bms.setItemTitle(itemId, val);
  607.         break;
  608.       case "bmkUri":
  609.         this._bms.changeBookmarkURI(itemId, Utils.makeURI(val));
  610.         break;
  611.       case "tags":
  612.         this._tagURI(this._bms.getBookmarkURI(itemId), val);
  613.         break;
  614.       case "keyword":
  615.         this._bms.setKeywordForBookmark(itemId, val);
  616.         break;
  617.       case "description":
  618.         Utils.anno(itemId, "bookmarkProperties/description", val);
  619.         break;
  620.       case "loadInSidebar":
  621.         if (val)
  622.           Utils.anno(itemId, "bookmarkProperties/loadInSidebar", true);
  623.         else
  624.           Svc.Annos.removeItemAnnotation(itemId, "bookmarkProperties/loadInSidebar");
  625.         break;
  626.       case "generatorUri": {
  627.         try {
  628.           let micsumURI = this._bms.getBookmarkURI(itemId);
  629.           let genURI = Utils.makeURI(val);
  630.       if (this._ms == SERVICE_NOT_SUPPORTED) {
  631.         this._log.warn("Can't create microsummary -- not supported.");
  632.       } else {
  633.             let micsum = this._ms.createMicrosummary(micsumURI, genURI);
  634.             this._ms.setMicrosummary(itemId, micsum);
  635.       }
  636.         } catch (e) {
  637.           this._log.debug("Could not set microsummary generator URI: " + e);
  638.         }
  639.       } break;
  640.       case "siteUri":
  641.         this._ls.setSiteURI(itemId, Utils.makeURI(val));
  642.         break;
  643.       case "feedUri":
  644.         this._ls.setFeedURI(itemId, Utils.makeURI(val));
  645.         break;
  646.       }
  647.     }
  648.   },
  649.  
  650.   changeItemID: function BStore_changeItemID(oldID, newID) {
  651.     // Remember the GUID change for incoming records and avoid invalid refs
  652.     this.aliases[oldID] = newID;
  653.     this.cache.clear();
  654.  
  655.     // Update any existing annotation references
  656.     this._findAnnoItems(PARENT_ANNO, oldID).forEach(function(itemId) {
  657.       Utils.anno(itemId, PARENT_ANNO, "T" + newID);
  658.     }, this);
  659.     this._findAnnoItems(PREDECESSOR_ANNO, oldID).forEach(function(itemId) {
  660.       Utils.anno(itemId, PREDECESSOR_ANNO, "R" + newID);
  661.     }, this);
  662.  
  663.     // Make sure there's an item to change GUIDs
  664.     let itemId = idForGUID(oldID);
  665.     if (itemId <= 0)
  666.       return;
  667.  
  668.     this._log.debug("Changing GUID " + oldID + " to " + newID);
  669.     this._setGUID(itemId, newID);
  670.   },
  671.  
  672.   _setGUID: function BStore__setGUID(itemId, guid) {
  673.     let collision = idForGUID(guid);
  674.     if (collision != -1) {
  675.       this._log.warn("Freeing up GUID " + guid  + " used by " + collision);
  676.       Svc.Annos.removeItemAnnotation(collision, "placesInternal/GUID");
  677.     }
  678.     Svc.Bookmark.setItemGUID(itemId, guid);
  679.   },
  680.  
  681.   _getNode: function BStore__getNode(folder) {
  682.     let query = this._hsvc.getNewQuery();
  683.     query.setFolders([folder], 1);
  684.     return this._hsvc.executeQuery(query, this._hsvc.getNewQueryOptions()).root;
  685.   },
  686.  
  687.   _getTags: function BStore__getTags(uri) {
  688.     try {
  689.       if (typeof(uri) == "string")
  690.         uri = Utils.makeURI(uri);
  691.     } catch(e) {
  692.       this._log.warn("Could not parse URI \"" + uri + "\": " + e);
  693.     }
  694.     return this._ts.getTagsForURI(uri, {});
  695.   },
  696.  
  697.   _getDescription: function BStore__getDescription(id) {
  698.     try {
  699.       return Utils.anno(id, "bookmarkProperties/description");
  700.     } catch (e) {
  701.       return undefined;
  702.     }
  703.   },
  704.  
  705.   _isLoadInSidebar: function BStore__isLoadInSidebar(id) {
  706.     return Svc.Annos.itemHasAnnotation(id, "bookmarkProperties/loadInSidebar");
  707.   },
  708.  
  709.   _getStaticTitle: function BStore__getStaticTitle(id) {
  710.     try {
  711.       return Utils.anno(id, "bookmarks/staticTitle");
  712.     } catch (e) {
  713.       return "";
  714.     }
  715.   },
  716.  
  717.   // Create a record starting from the weave id (places guid)
  718.   createRecord: function BStore_createRecord(guid, cryptoMetaURL) {
  719.     let record = this.cache.get(guid);
  720.     if (record)
  721.       return record;
  722.  
  723.     let placeId = idForGUID(guid);
  724.     if (placeId <= 0) { // deleted item
  725.       record = new PlacesItem();
  726.       record.id = guid;
  727.       record.deleted = true;
  728.       this.cache.put(guid, record);
  729.       return record;
  730.     }
  731.  
  732.     let parent = Svc.Bookmark.getFolderIdForItem(placeId);
  733.     switch (this._bms.getItemType(placeId)) {
  734.     case this._bms.TYPE_BOOKMARK:
  735.       let bmkUri = this._bms.getBookmarkURI(placeId).spec;
  736.       if (this._ms && this._ms.hasMicrosummary(placeId)) {
  737.         record = new BookmarkMicsum();
  738.         let micsum = this._ms.getMicrosummary(placeId);
  739.         record.generatorUri = micsum.generator.uri.spec; // breaks local generators
  740.         record.staticTitle = this._getStaticTitle(placeId);
  741.       }
  742.       else {
  743.         if (bmkUri.search(/^place:/) == 0) {
  744.           record = new BookmarkQuery();
  745.  
  746.           // Get the actual tag name instead of the local itemId
  747.           let folder = bmkUri.match(/[:&]folder=(\d+)/);
  748.           try {
  749.             // There might not be the tag yet when creating on a new client
  750.             if (folder != null) {
  751.               folder = folder[1];
  752.               record.folderName = this._bms.getItemTitle(folder);
  753.               this._log.debug("query id: " + folder + " = " + record.folderName);
  754.             }
  755.           }
  756.           catch(ex) {}
  757.         }
  758.         else
  759.           record = new Bookmark();
  760.         record.title = this._bms.getItemTitle(placeId);
  761.       }
  762.  
  763.       record.parentName = Svc.Bookmark.getItemTitle(parent);
  764.       record.bmkUri = bmkUri;
  765.       record.tags = this._getTags(record.bmkUri);
  766.       record.keyword = this._bms.getKeywordForBookmark(placeId);
  767.       record.description = this._getDescription(placeId);
  768.       record.loadInSidebar = this._isLoadInSidebar(placeId);
  769.       break;
  770.  
  771.     case this._bms.TYPE_FOLDER:
  772.       if (this._ls.isLivemark(placeId)) {
  773.         record = new Livemark();
  774.  
  775.         let siteURI = this._ls.getSiteURI(placeId);
  776.         if (siteURI != null)
  777.           record.siteUri = siteURI.spec;
  778.         record.feedUri = this._ls.getFeedURI(placeId).spec;
  779.  
  780.       } else {
  781.         record = new BookmarkFolder();
  782.       }
  783.  
  784.       record.parentName = Svc.Bookmark.getItemTitle(parent);
  785.       record.title = this._bms.getItemTitle(placeId);
  786.       break;
  787.  
  788.     case this._bms.TYPE_SEPARATOR:
  789.       record = new BookmarkSeparator();
  790.       // Create a positioning identifier for the separator
  791.       record.parentName = Svc.Bookmark.getItemTitle(parent);
  792.       record.pos = Svc.Bookmark.getItemIndex(placeId);
  793.       break;
  794.  
  795.     case this._bms.TYPE_DYNAMIC_CONTAINER:
  796.       record = new PlacesItem();
  797.       this._log.warn("Don't know how to serialize dynamic containers yet");
  798.       break;
  799.  
  800.     default:
  801.       record = new PlacesItem();
  802.       this._log.warn("Unknown item type, cannot serialize: " +
  803.                      this._bms.getItemType(placeId));
  804.     }
  805.  
  806.     record.id = guid;
  807.     record.parentid = this._getParentGUIDForId(placeId);
  808.     record.predecessorid = this._getPredecessorGUIDForId(placeId);
  809.     record.encryption = cryptoMetaURL;
  810.     record.sortindex = this._calculateIndex(record);
  811.  
  812.     this.cache.put(guid, record);
  813.     return record;
  814.   },
  815.  
  816.   get _frecencyStm() {
  817.     this._log.trace("Creating SQL statement: _frecencyStm");
  818.     let stm = Svc.History.DBConnection.createStatement(
  819.       "SELECT frecency " +
  820.       "FROM moz_places_view " +
  821.       "WHERE url = :url");
  822.     this.__defineGetter__("_frecencyStm", function() stm);
  823.     return stm;
  824.   },
  825.  
  826.   _calculateIndex: function _calculateIndex(record) {
  827.     // For anything directly under the toolbar, give it a boost of more than an
  828.     // unvisited bookmark
  829.     let index = 0;
  830.     if (record.parentid == "toolbar")
  831.       index += 150;
  832.  
  833.     // Add in the bookmark's frecency if we have something
  834.     if (record.bmkUri != null) {
  835.       try {
  836.         this._frecencyStm.params.url = record.bmkUri;
  837.         if (this._frecencyStm.step())
  838.           index += this._frecencyStm.row.frecency;
  839.       }
  840.       finally {
  841.         this._frecencyStm.reset();
  842.       }
  843.     }
  844.  
  845.     return index;
  846.   },
  847.  
  848.   _getParentGUIDForId: function BStore__getParentGUIDForId(itemId) {
  849.     // Give the parent annotation if it exists
  850.     try {
  851.       // XXX Work around Bug 510628 by removing prepended parenT
  852.       return Utils.anno(itemId, PARENT_ANNO).slice(1);
  853.     }
  854.     catch(ex) {}
  855.  
  856.     let parentid = this._bms.getFolderIdForItem(itemId);
  857.     if (parentid == -1) {
  858.       this._log.debug("Found orphan bookmark, reparenting to unfiled");
  859.       parentid = this._bms.unfiledBookmarksFolder;
  860.       this._bms.moveItem(itemId, parentid, -1);
  861.     }
  862.     return GUIDForId(parentid);
  863.   },
  864.  
  865.   _getPredecessorGUIDForId: function BStore__getPredecessorGUIDForId(itemId) {
  866.     // Give the predecessor annotation if it exists
  867.     try {
  868.       // XXX Work around Bug 510628 by removing prepended predecessoR
  869.       return Utils.anno(itemId, PREDECESSOR_ANNO).slice(1);
  870.     }
  871.     catch(ex) {}
  872.  
  873.     // Figure out the predecessor, unless it's the first item
  874.     let itemPos = Svc.Bookmark.getItemIndex(itemId);
  875.     if (itemPos == 0)
  876.       return;
  877.  
  878.     // For items directly under unfiled/unsorted, give no predecessor
  879.     let parentId = Svc.Bookmark.getFolderIdForItem(itemId);
  880.     if (parentId == Svc.Bookmark.unfiledBookmarksFolder)
  881.       return;
  882.  
  883.     let predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos - 1);
  884.     if (predecessorId == -1) {
  885.       this._log.debug("No predecessor directly before " + itemId + " under " +
  886.         parentId + " at " + itemPos);
  887.  
  888.       // Find the predecessor before the item
  889.       do {
  890.         // No more items to check, it must be the first one
  891.         if (--itemPos < 0)
  892.           break;
  893.         predecessorId = Svc.Bookmark.getIdForItemAt(parentId, itemPos);
  894.       } while (predecessorId == -1);
  895.  
  896.       // Fix up the item to be at the right position for next time
  897.       itemPos++;
  898.       this._log.debug("Fixing " + itemId + " to be at position " + itemPos);
  899.       Svc.Bookmark.moveItem(itemId, parentId, itemPos);
  900.  
  901.       // There must be no predecessor for this item!
  902.       if (itemPos == 0)
  903.         return;
  904.     }
  905.  
  906.     return GUIDForId(predecessorId);
  907.   },
  908.  
  909.   _getChildren: function BStore_getChildren(guid, items) {
  910.     let node = guid; // the recursion case
  911.     if (typeof(node) == "string") // callers will give us the guid as the first arg
  912.       node = this._getNode(idForGUID(guid));
  913.  
  914.     if (node.type == node.RESULT_TYPE_FOLDER &&
  915.         !this._ls.isLivemark(node.itemId)) {
  916.       node.QueryInterface(Ci.nsINavHistoryQueryResultNode);
  917.       node.containerOpen = true;
  918.  
  919.       // Remember all the children GUIDs and recursively get more
  920.       for (var i = 0; i < node.childCount; i++) {
  921.         let child = node.getChild(i);
  922.         items[GUIDForId(child.itemId)] = true;
  923.         this._getChildren(child, items);
  924.       }
  925.     }
  926.  
  927.     return items;
  928.   },
  929.  
  930.   _tagURI: function BStore_tagURI(bmkURI, tags) {
  931.     // Filter out any null/undefined/empty tags
  932.     tags = tags.filter(function(t) t);
  933.  
  934.     // Temporarily tag a dummy uri to preserve tag ids when untagging
  935.     let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
  936.     this._ts.tagURI(dummyURI, tags);
  937.     this._ts.untagURI(bmkURI, null);
  938.     this._ts.tagURI(bmkURI, tags);
  939.     this._ts.untagURI(dummyURI, null);
  940.   },
  941.  
  942.   getAllIDs: function BStore_getAllIDs() {
  943.     let items = {};
  944.     for (let [guid, id] in Iterator(kSpecialIds))
  945.       if (guid != "places" && guid != "tags")
  946.         this._getChildren(guid, items);
  947.     return items;
  948.   },
  949.  
  950.   wipe: function BStore_wipe() {
  951.     // some nightly builds of 3.7 don't have this function
  952.     try {
  953.       PlacesUtils.archiveBookmarksFile(null, true);
  954.     } catch(e) {}
  955.  
  956.     for (let [guid, id] in Iterator(kSpecialIds))
  957.       if (guid != "places")
  958.         this._bms.removeFolderChildren(id);
  959.   }
  960. };
  961.  
  962. function BookmarksTracker() {
  963.   this._init();
  964. }
  965. BookmarksTracker.prototype = {
  966.   __proto__: Tracker.prototype,
  967.   name: "bookmarks",
  968.   _logName: "BmkTracker",
  969.   file: "bookmarks",
  970.  
  971.   get _bms() {
  972.     let bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
  973.       getService(Ci.nsINavBookmarksService);
  974.     this.__defineGetter__("_bms", function() bms);
  975.     return bms;
  976.   },
  977.  
  978.   get _ls() {
  979.     let ls = Cc["@mozilla.org/browser/livemark-service;2"].
  980.       getService(Ci.nsILivemarkService);
  981.     this.__defineGetter__("_ls", function() ls);
  982.     return ls;
  983.   },
  984.  
  985.   QueryInterface: XPCOMUtils.generateQI([
  986.     Ci.nsINavBookmarkObserver,
  987.     Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS
  988.   ]),
  989.  
  990.   _init: function BMT__init() {
  991.     this.__proto__.__proto__._init.call(this);
  992.  
  993.     // Ignore changes to the special roots
  994.     for (let guid in kSpecialIds)
  995.       this.ignoreID(guid);
  996.  
  997.     this._bms.addObserver(this, false);
  998.   },
  999.  
  1000.   /**
  1001.    * Add a bookmark (places) id to be uploaded and bump up the sync score
  1002.    *
  1003.    * @param itemId
  1004.    *        Places internal id of the bookmark to upload
  1005.    */
  1006.   _addId: function BMT__addId(itemId) {
  1007.     if (this.addChangedID(GUIDForId(itemId)))
  1008.       this._upScore();
  1009.   },
  1010.  
  1011.   /**
  1012.    * Add the successor id for the item that follows the given item
  1013.    */
  1014.   _addSuccessor: function BMT__addSuccessor(itemId) {
  1015.     let parentId = Svc.Bookmark.getFolderIdForItem(itemId);
  1016.     let itemPos = Svc.Bookmark.getItemIndex(itemId);
  1017.     let succId = Svc.Bookmark.getIdForItemAt(parentId, itemPos + 1);
  1018.     if (succId != -1)
  1019.       this._addId(succId);
  1020.   },
  1021.  
  1022.   /* Every add/remove/change is worth 10 points */
  1023.   _upScore: function BMT__upScore() {
  1024.     this.score += 10;
  1025.   },
  1026.  
  1027.   /**
  1028.    * Determine if a change should be ignored: we're ignoring everything or the
  1029.    * folder is for livemarks
  1030.    *
  1031.    * @param itemId
  1032.    *        Item under consideration to ignore
  1033.    * @param folder (optional)
  1034.    *        Folder of the item being changed
  1035.    */
  1036.   _ignore: function BMT__ignore(itemId, folder) {
  1037.     // Ignore unconditionally if the engine tells us to
  1038.     if (this.ignoreAll)
  1039.       return true;
  1040.  
  1041.     // Ensure that the mobile bookmarks query is correct in the UI
  1042.     this._ensureMobileQuery();
  1043.  
  1044.     // Make sure to remove items that have the exclude annotation
  1045.     if (Svc.Annos.itemHasAnnotation(itemId, "places/excludeFromBackup")) {
  1046.       this.removeChangedID(GUIDForId(itemId));
  1047.       return true;
  1048.     }
  1049.  
  1050.     // Get the folder id if we weren't given one
  1051.     if (folder == null)
  1052.       folder = this._bms.getFolderIdForItem(itemId);
  1053.  
  1054.     let tags = kSpecialIds.tags;
  1055.     // Ignore changes to tags (folders under the tags folder)
  1056.     if (folder == tags)
  1057.       return true;
  1058.  
  1059.     // Ignore tag items (the actual instance of a tag for a bookmark)
  1060.     if (this._bms.getFolderIdForItem(folder) == tags)
  1061.       return true;
  1062.  
  1063.     // Ignore livemark children
  1064.     return this._ls.isLivemark(folder);
  1065.   },
  1066.  
  1067.   onItemAdded: function BMT_onEndUpdateBatch(itemId, folder, index) {
  1068.     if (this._ignore(itemId, folder))
  1069.       return;
  1070.  
  1071.     this._log.trace("onItemAdded: " + itemId);
  1072.     this._addId(itemId);
  1073.     this._addSuccessor(itemId);
  1074.   },
  1075.  
  1076.   onBeforeItemRemoved: function BMT_onBeforeItemRemoved(itemId) {
  1077.     if (this._ignore(itemId))
  1078.       return;
  1079.  
  1080.     this._log.trace("onBeforeItemRemoved: " + itemId);
  1081.     this._addId(itemId);
  1082.     this._addSuccessor(itemId);
  1083.   },
  1084.  
  1085.   _ensureMobileQuery: function _ensureMobileQuery() {
  1086.     let anno = "PlacesOrganizer/OrganizerQuery";
  1087.     let find = function(val) Svc.Annos.getItemsWithAnnotation(anno, {}).filter(
  1088.       function(id) Utils.anno(id, anno) == val);
  1089.  
  1090.     // Don't continue if the Library isn't ready
  1091.     let all = find("AllBookmarks");
  1092.     if (all.length == 0)
  1093.       return;
  1094.  
  1095.     // Disable handling of notifications while changing the mobile query
  1096.     this.ignoreAll = true;
  1097.  
  1098.     let mobile = find("MobileBookmarks");
  1099.     let queryURI = Utils.makeURI("place:folder=" + kSpecialIds.mobile);
  1100.     let title = Str.sync.get("mobile.label");
  1101.  
  1102.     // Don't add OR do remove the mobile bookmarks if there's nothing
  1103.     if (Svc.Bookmark.getIdForItemAt(kSpecialIds.mobile, 0) == -1) {
  1104.       if (mobile.length != 0)
  1105.         Svc.Bookmark.removeItem(mobile[0]);
  1106.     }
  1107.     // Add the mobile bookmarks query if it doesn't exist
  1108.     else if (mobile.length == 0) {
  1109.       let query = Svc.Bookmark.insertBookmark(all[0], queryURI, -1, title);
  1110.       Utils.anno(query, anno, "MobileBookmarks");
  1111.       Utils.anno(query, "places/excludeFromBackup", 1);
  1112.     }
  1113.     // Make sure the existing title is correct
  1114.     else if (Svc.Bookmark.getItemTitle(mobile[0]) != title)
  1115.       Svc.Bookmark.setItemTitle(mobile[0], title);
  1116.  
  1117.     this.ignoreAll = false;
  1118.   },
  1119.  
  1120.   onItemChanged: function BMT_onItemChanged(itemId, property, isAnno, value) {
  1121.     if (this._ignore(itemId))
  1122.       return;
  1123.  
  1124.     // ignore annotations except for the ones that we sync
  1125.     let annos = ["bookmarkProperties/description",
  1126.       "bookmarkProperties/loadInSidebar", "bookmarks/staticTitle",
  1127.       "livemark/feedURI", "livemark/siteURI", "microsummary/generatorURI"];
  1128.     if (isAnno && annos.indexOf(property) == -1)
  1129.       return;
  1130.  
  1131.     // Ignore favicon changes to avoid unnecessary churn
  1132.     if (property == "favicon")
  1133.       return;
  1134.  
  1135.     this._log.trace("onItemChanged: " + itemId +
  1136.                     (", " + property + (isAnno? " (anno)" : "")) +
  1137.                     (value? (" = \"" + value + "\"") : ""));
  1138.     this._addId(itemId);
  1139.   },
  1140.  
  1141.   onItemMoved: function BMT_onItemMoved(itemId, oldParent, oldIndex, newParent, newIndex) {
  1142.     if (this._ignore(itemId))
  1143.       return;
  1144.  
  1145.     this._log.trace("onItemMoved: " + itemId);
  1146.     this._addId(itemId);
  1147.     this._addSuccessor(itemId);
  1148.  
  1149.     // Get the thing that's now at the old place
  1150.     let oldSucc = Svc.Bookmark.getIdForItemAt(oldParent, oldIndex);
  1151.     if (oldSucc != -1)
  1152.       this._addId(oldSucc);
  1153.   },
  1154.  
  1155.   onBeginUpdateBatch: function BMT_onBeginUpdateBatch() {},
  1156.   onEndUpdateBatch: function BMT_onEndUpdateBatch() {},
  1157.   onItemRemoved: function BMT_onItemRemoved(itemId, folder, index) {},
  1158.   onItemVisited: function BMT_onItemVisited(itemId, aVisitID, time) {}
  1159. };
  1160.